package distribution;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 最高位：0
 * 时间：40bits
 * 机器：16bits
 * 序列：7bits
 */
public class IdHelper {

    private IdHelper() {

    }

    private final static long start_time = getTime("2020-01-01 00:00:00").getTime();
    private final static long max_time_backwards_seconds = TimeUnit.SECONDS.toMillis(3);
    private static int machine;
    private volatile static int seq = 0;
    private volatile static long lastTime;
    private final static SecureRandom secureRandom = new SecureRandom();

    static {
        try {
            String localHost = getLocalHost();
            String[] split = localHost.split("\\.");
            machine = (Integer.parseInt(split[2]) << 8) | (Integer.parseInt(split[3]));
            lastTime = start_time;
        } catch (Exception ex) {
            //兜底
            machine = secureRandom.nextInt(0xffff + 1) & 0xffff;
        }
    }

    /**
     * 此id只能保证同一个集群内不重复，
     * 但是不同集群间生成的id有可能是重复的。
     *
     * @return
     */
    public static synchronized long genId() {
        long current = System.currentTimeMillis();

        if (current > lastTime) {
            //时间不同，id一定不相同，重置seq
            //如果不重置seq，那么seq会越来越大，也许会发生预料之外的逻辑
            seq = 0;
        } else if (current == lastTime) {
            //时间相同
            if (seq <= 127) {
                //在相同时间内，seq继续被使用;
            } else {
                //7bits的seq在一毫秒内发生了重复，可能导致id碰撞，那么等待时间
                //不可以直接把时间+1进位处理，否则下次执行该方法的时候，时间可能还是本次执行的时间，那么分析程序如何执行就会变得困难。
                current = tilNextMillis(lastTime);
                seq = 0;
            }
        } else {
            //发生了时间回拨，可能导致id重复
            if (lastTime - current > max_time_backwards_seconds) {
                throw new RuntimeException("时间回拨大于3秒，请检查服务器的时钟");
            }
            current = tilNextMillis(lastTime);
            seq = 0;
        }

        long id = (current - start_time) << 23 | (machine << 7) | (seq & 0x7f);

        lastTime = current;
        seq++;

        return id;
    }

    public static int getMachineId() {
        return machine;
    }

    /**
     * 生成128bits单调递增id，字符串比较序。
     * <p>
     * <br>
     * 生成单调递增id的时空间：时间+机器+JVM。
     * <p>
     * 重复性质：<br>
     * （1）时间不同，id一定不同；<br>
     * （2）同一机器的相同JVM内的id一定不冲突；<br>
     * （3）同一机器的不同JVM内几乎不冲突；<br>
     * （4）不同机器的id一定不冲突；<br>
     * <br>
     * 排序性质：<br>
     * （1）首先根据时间排序，精度是1ms；<br>
     * （2）时间相同时，根据机器排序；<br>
     * （3）机器相同时，根据序列号排序；<br>
     * （4）序列号相同时，说明是不同的JVM，即相同时间相同机器不同JVM条件下，根据随机数排序；<br>
     * <p>
     * 3000个服务集群在一毫秒内产生冲突的个数不到千分之一，估计背景为整体QPS为2亿/ms。
     *
     * @return
     */
    public static synchronized String genMonoId() {
        long sortId = genId();
        long randomSortId = secureRandom.nextLong();
        UUID uuid = new UUID(sortId, randomSortId);
        return uuid.toString().replaceAll("-", "").toUpperCase(Locale.ENGLISH);
    }

    public static Date extractTimeFromMonoId(String monoId) {
        String s = monoId.toLowerCase(Locale.ENGLISH);
        String substring = s.substring(0, 16);
        long l = Long.parseLong(substring, 16);
        long interval = l >> 23;
        return new Date(start_time + interval);
    }

    /**
     * 阻塞到下一个毫秒，直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    private static long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    private static long timeGen() {
        return System.currentTimeMillis();
    }

    private static Date getTime(String time) {
        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
            simpleDateFormat.applyPattern("yyyy-MM-dd HH:mm:ss");
            return simpleDateFormat.parse(time);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private static String getLocalHost() throws SocketException {
        return getLocalHosts().get(0);
    }

    private static List<String> getLocalHosts() throws SocketException {
        List<String> ipList = new ArrayList();
        Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
        while (allNetInterfaces.hasMoreElements()) {
            NetworkInterface netInterface = allNetInterfaces.nextElement();
            Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
            while (addresses.hasMoreElements()) {
                InetAddress ip = addresses.nextElement();
                if (!ip.isLinkLocalAddress() && !ip.isLoopbackAddress() && ip instanceof Inet4Address) {
                    ipList.add(ip.getHostAddress());
                }
            }
        }
        return ipList;
    }
}
